diff options
| author | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-24 13:09:50 +0000 |
| commit | 396acf3bbbe00a192cb0ea0a9ccf91b1d8d2850b (patch) | |
| tree | b9df4ca6a70db45cfffbae6fdd7252e20fb8e93c /src/app/(main)/websites/[websiteId]/(reports)/attribution | |
| download | umami-main.tar.xz umami-main.zip | |
Created from https://vercel.com/new
Diffstat (limited to 'src/app/(main)/websites/[websiteId]/(reports)/attribution')
3 files changed, 203 insertions, 0 deletions
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx new file mode 100644 index 0000000..264923a --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/Attribution.tsx @@ -0,0 +1,128 @@ +import { Column, Grid } from '@umami/react-zen'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { Panel } from '@/components/common/Panel'; +import { SectionHeader } from '@/components/common/SectionHeader'; +import { useMessages, useResultQuery } from '@/components/hooks'; +import { ListTable } from '@/components/metrics/ListTable'; +import { MetricCard } from '@/components/metrics/MetricCard'; +import { MetricsBar } from '@/components/metrics/MetricsBar'; +import { percentFilter } from '@/lib/filters'; +import { formatLongNumber } from '@/lib/format'; + +export interface AttributionProps { + websiteId: string; + startDate: Date; + endDate: Date; + model: string; + type: string; + step: string; + currency?: string; +} + +export function Attribution({ + websiteId, + startDate, + endDate, + model, + type, + step, + currency, +}: AttributionProps) { + const { data, error, isLoading } = useResultQuery<any>('attribution', { + websiteId, + startDate, + endDate, + model, + type, + step, + }); + + const { formatMessage, labels } = useMessages(); + + const { pageviews, visitors, visits } = data?.total || {}; + + const metrics = data + ? [ + { + value: visitors, + label: formatMessage(labels.visitors), + formatValue: formatLongNumber, + }, + { + value: visits, + label: formatMessage(labels.visits), + formatValue: formatLongNumber, + }, + { + value: pageviews, + label: formatMessage(labels.views), + formatValue: formatLongNumber, + }, + ] + : []; + + function AttributionTable({ data = [], title }: { data: any; title: string }) { + const attributionData = percentFilter( + data.map(({ name, value }) => ({ + x: name, + y: Number(value), + })), + ); + + return ( + <ListTable + title={title} + metric={formatMessage(currency ? labels.revenue : labels.visitors)} + currency={currency} + data={attributionData.map(({ x, y, z }: { x: string; y: number; z: number }) => ({ + label: x, + count: y, + percent: z, + }))} + /> + ); + } + + return ( + <LoadingPanel data={data} isLoading={isLoading} error={error}> + {data && ( + <Column gap> + <MetricsBar> + {metrics?.map(({ label, value, formatValue }) => { + return ( + <MetricCard key={label} value={value} label={label} formatValue={formatValue} /> + ); + })} + </MetricsBar> + <SectionHeader title={formatMessage(labels.sources)} /> + <Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap> + <Panel> + <AttributionTable data={data?.referrer} title={formatMessage(labels.referrer)} /> + </Panel> + <Panel> + <AttributionTable data={data?.paidAds} title={formatMessage(labels.paidAds)} /> + </Panel> + </Grid> + <SectionHeader title="UTM" /> + <Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap> + <Panel> + <AttributionTable data={data?.utm_source} title={formatMessage(labels.sources)} /> + </Panel> + <Panel> + <AttributionTable data={data?.utm_medium} title={formatMessage(labels.medium)} /> + </Panel> + <Panel> + <AttributionTable data={data?.utm_cmapaign} title={formatMessage(labels.campaigns)} /> + </Panel> + <Panel> + <AttributionTable data={data?.utm_content} title={formatMessage(labels.content)} /> + </Panel> + <Panel> + <AttributionTable data={data?.utm_term} title={formatMessage(labels.terms)} /> + </Panel> + </Grid> + </Column> + )} + </LoadingPanel> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx new file mode 100644 index 0000000..48611c4 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage.tsx @@ -0,0 +1,63 @@ +'use client'; +import { Column, Grid, ListItem, SearchField, Select } from '@umami/react-zen'; +import { useState } from 'react'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { useDateRange, useMessages } from '@/components/hooks'; +import { Attribution } from './Attribution'; + +export function AttributionPage({ websiteId }: { websiteId: string }) { + const [model, setModel] = useState('first-click'); + const [type, setType] = useState('path'); + const [step, setStep] = useState('/'); + const { formatMessage, labels } = useMessages(); + const { + dateRange: { startDate, endDate }, + } = useDateRange(); + + return ( + <Column gap="6"> + <WebsiteControls websiteId={websiteId} /> + <Grid columns={{ xs: '1fr', md: '1fr 1fr 1fr' }} gap> + <Column> + <Select + label={formatMessage(labels.model)} + value={model} + defaultValue={model} + onChange={setModel} + > + <ListItem id="first-click">{formatMessage(labels.firstClick)}</ListItem> + <ListItem id="last-click">{formatMessage(labels.lastClick)}</ListItem> + </Select> + </Column> + <Column> + <Select + label={formatMessage(labels.type)} + value={type} + defaultValue={type} + onChange={setType} + > + <ListItem id="path">{formatMessage(labels.viewedPage)}</ListItem> + <ListItem id="event">{formatMessage(labels.triggeredEvent)}</ListItem> + </Select> + </Column> + <Column> + <SearchField + label={formatMessage(labels.conversionStep)} + value={step} + defaultValue={step} + onSearch={setStep} + delay={1000} + /> + </Column> + </Grid> + <Attribution + websiteId={websiteId} + startDate={startDate} + endDate={endDate} + model={model} + type={type} + step={step} + /> + </Column> + ); +} diff --git a/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx new file mode 100644 index 0000000..1368d4b --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/(reports)/attribution/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import { AttributionPage } from './AttributionPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return <AttributionPage websiteId={websiteId} />; +} + +export const metadata: Metadata = { + title: 'Attribution', +}; |